﻿/*
 * Copyright 2011 Edward L. Stauff.                           Contact: <EdStauff@gmail.com>.
 * 
 * This file, "XerialListMapping.cs", is part of the "Stauffware.Common.Xml.Xerialization" 
 * library.  "Stauffware.Common.Xml.Xerialization" is free software: you can redistribute 
 * it and/or modify it under the terms of the GNU Lesser Public License as published by 
 * the Free Software Foundation, either version 3 of the License, or (at your option) 
 * any later version.
 * 
 * "Stauffware.Common.Xml.Xerialization" is distributed in the hope that it will be 
 * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser Public License for more 
 * details.
 * 
 * You should have received a copy of the GNU Lesser Public License along with 
 * "Stauffware.Common.Xml.Xerialization".  If not, see <http://www.gnu.org/licenses/>.
 */
using System;
using System.Collections;
using System.Collections.Generic;
using System.Xml;
using System.Text;
using System.Reflection;
using Stauffware.Common.Core;

namespace Stauffware.Common.Xml.Xerialization
{
	/// <summary>
	/// Provides a mapping between XML and a .Net list class.
	/// </summary>
	public class XerialListMapping : XerialClassMapping
	{
		/// <inherit/>
		public override bool AllowsXmlElmnt { get { return true; } }
		/// <inherit/>
		public override bool AllowsXmlAttr { get { return false; } }
		/// <inherit/>
		public override bool AllowsXmlText { get { return false; } }

		/// <summary>
		/// The type mapping for list items.
		/// </summary>
		public readonly XerialTypeMapping ItemMapping;

		private readonly ConstructorInfo Constructor;
		private readonly MethodInfo Adder;

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Tries to create a XerialListMapping for the given type.  Returns null if it can't.
		/// </summary>

		public static XerialListMapping TryCreate (XerialContext context, Type targetType, string xmlName)
		{
			if (typeof(IList).IsAssignableFrom(targetType))
				return new XerialListMapping(context, targetType, xmlName);


			// may need to handle other cases


			return null;
		}

		//-----------------------------------------------------------------------------
		/// <summary> Constructor </summary>

		public XerialListMapping (XerialContext context, Type targetType, string xmlName)
			: base(context, targetType, 
				   xmlName == null ? CreateDefaultXmlName(targetType) : xmlName /*, 
				   XmlType.Element */ )
		{
			Require.Condition(typeof(IList).IsAssignableFrom(targetType));

			this.Constructor = this.TargetType.GetConstructor(System.Type.EmptyTypes);
			Require.NotNull(this.Constructor, "no parameterless constructor for " + targetType.FullName);

			Type[] paramTypes = new Type[1];

			if (targetType.IsGenericType || targetType.IsGenericTypeDefinition)
			{
				Type[] gArgs = targetType.GetGenericArguments();
				Assert.Condition(gArgs.Length == 1, "can't handle generic types with more than one type specifier");
				paramTypes[0] = gArgs[0];
			}
			else paramTypes[0] = targetType.GetElementType();

			this.Adder = targetType.GetMethod("Add", paramTypes);
			Require.NotNull(this.Constructor, "no suitable 'Add' method for " + targetType.FullName);

			this.ItemMapping = this.Context.TypeMappings.GetOrAdd(paramTypes[0]);
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Returns a type mapping appropriate for the given object.  Never returns null.
		/// </summary>

		protected XerialTypeMapping GetItemTypeMapping (object targetObj)
		{
			if (targetObj == null)
				return this.ItemMapping;
			Type targetType = targetObj.GetType();
			Assert.Condition(this.ItemMapping.TargetType.IsAssignableFrom(targetType));
			XerialTypeMapping tp = this.Context.TypeMappings[targetType];
			if (tp == null)
				throw new XerialException("don't know how to xerialize type " + targetType.FullName);
			return tp;
		}

		//-----------------------------------------------------------------------------
		/// <inherit/>

		public override void WriteToXmlElmnt (string xmlName, object targetObj, XmlWriter writer)
		{
			if (targetObj == null)
				return;

			writer.WriteStartElement(xmlName == null ? this.XmlName : xmlName);

			foreach (object item in ((ICollection)targetObj))
				this.GetItemTypeMapping(item).WriteToXmlElmnt(null, item, writer);

			writer.WriteEndElement();
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Reads a list object from one complete XML element (which may include attributes, 
		/// child elements, and/or text).
		/// </summary>
		/// <param name="reader">
		///		Where to read the XML from.
		///		On entry, the current node must be the Element that contains the entire list.
		///		On exit, the current node will be the node immediately following the list Element
		///			(for an empty Element) or the corresponding EndElement (for a non-empty element).
		/// </param>
		/// <returns>the newly created list object</returns>

		public override object ReadFromXmlElmnt (NiceXmlReader reader)
		{
			if (DebugLogEnabled) this.LogCall("ReadElement", reader);

			reader.MoveToContent();
			reader.Expect(XmlNodeType.Element);
			string elName = reader.Name;

			if (DebugLogEnabled) this.LogLine("        instantiate " + this.TargetType.FullName + ";  ", reader);

			ICollection newColl = (ICollection)this.Constructor.Invoke(null);

			object[] adderParams = new object[1];

			if (!reader.IsEmptyElement)
			{
				reader.Read();
				reader.MoveToContent();
				int n = 0;

				while (reader.NodeType != XmlNodeType.EndElement)
				{
					if (DebugLogEnabled) this.LogLine("     [" + n.ToString() + "] reading item; ", reader);

					XerialTypeMapping tm = this.Context.TypeMappings[reader.Name];
					if (tm == null)
						throw new XerialException(reader, "\"" + reader.Name + 
									"\" is not a known XML element name; did you forget to register a type?");
					if (!tm.IsOrDerivesFrom(this.ItemMapping))
					{
						reader.BadXml(tm.XmlName + " does not derive from " + this.ItemMapping.XmlName);
						reader.Skip();
					}
					else
					{
						adderParams[0] =  tm.ReadFromXmlElmnt(reader);
						if (DebugLogEnabled) this.LogLine("     [" + n.ToString() + "] got a " + adderParams[0].GetType().Name + "; ", reader);
						this.Adder.Invoke(newColl, adderParams);
					}

					reader.MoveToContent();
					n++;
				}

				reader.ExpectEndElement(elName);
			}

			reader.Read();	// skip EndElement or empty Element

			if (DebugLogEnabled) this.LogCall("exiting ReadElement", reader);
			return newColl;
		}

		//-----------------------------------------------------------------------------
		
	}
}
